//
//  InputBoxManager.swift
//  Swift-Codedase
//
//  Created by xyanl on 2024/8/28.
//  Copyright © 2024 考拉🐨. All rights reserved.
//
// 顺便提一句：切换到软键盘是靠系统的 [textView becomeFirstResponder]; 然后会触发键盘的Notification，然后就进入我上面封装的 onKeyboardWillShowOrHideByNotifications 方法

import UIKit

enum InputState {
    // 默认状态，没弹出键盘 也没弹出表情 也没弹出extend面板
    case normal
    // 弹出软键盘状态
    case text
    // 弹出表情状态
    case emoticon
    // 弹出 extend 面板状态
    case extentd
}

enum InputViewHeightDidChangeReason: Int {
    /// 因为输入条被add到vc中
    case willAddToSuperView
    /// 因为文本框输入的内容高度发生变化
    case textDidChange
    /// 因为点“发送”按钮所以清空了文本框 2022-03-24与上面那条区分开
    case textDidSend
    /// 显示了 软键盘or面板（表情面板或者拓展面板）
    case boardDidShow
    /// 隐藏了 软键盘or面板（表情面板或者拓展面板）
    case boardDidHide
}

protocol InputBoardDataSource: AnyObject {
    /// 点加号按钮弹出的扩展面板，且无需设置frame
    func extendBoardView(keyboardManager: InputBoxManager) -> UIView
    /// 点表情按钮弹出的表情面板View，且无需设置frame
    func emoticonBoardView(keyboardManager: InputBoxManager) -> UIView
    
    /// 点加号按钮弹出的拓展面板View的高度
    func extendBoardHeight(keyboardManager: InputBoxManager) -> CGFloat
    /// 点表情按钮弹出的表情面板View的高度
    func emoticonBoardHeight(keyboardManager: InputBoxManager) -> CGFloat
    
}
//表示一下代码含有OC特性
@objc protocol InputBoardDelegate: AnyObject  {
    /// 整个输入View的高度发生变化（整个View包含bar和表情栏或者键盘，但是不包含底部安全区高度）
    /// 会触发这个的原因：1、addBottomInputBarView；2、输入文字换行了；3、切换面板；4、呼出键盘
    /// Warning：这个回调方法的触发已经在animate中了，无需再在本方法里写animate
    /// 更新当前 inputBar 的frame
    /// - Parameters:
    ///   - manager: manager description
    ///   - frame: frame description
    ///   - reson: reson description
    @objc optional func inputBoxManager(manager: InputBoxManager, currentInputBarFrame frame: CGRect, reson: Int)
}


class InputBoxManager: InputBoxBaseManager {
    
    weak var delegate: InputBoardDelegate?
    weak var dataSource: InputBoardDataSource?
    
    /// viewController 是否要走 viewWillDisappear；
    /// 为了解决：键盘弹出状态下vc侧滑返回，会触发 Notification 进而影响 inputBarView.frame 的 bug
    var isViewControllerWillDisappear: Bool = false
    
    
    /// 当前 vc
    // private override var viewController: UIViewController!
    /// true：输入条平时是不显示出来的；false：输入条一直都在vc底部
    private var isInputBarBelowViewController: Bool = false
    
    
    
    /// 当前的输入状态
    private var currentInputState: InputState = .normal
    
    /// 输入条，由vc传过来，一般情况下，这个输入框是 inputBar 的子 view ，但是也可能不是（比如发微博界面）
    private weak var inputTextView: UIResponder?
    
    // 那一条输入框bar，由vc传过来，包含了左右的按钮
    private weak var inputBarView: UIView!
    private lazy var emotionBoardView: UIView = { [self] in
        
        let subView = dataSource?.emoticonBoardView(keyboardManager: self) ?? UIView()
        // 默认高度 220
        let height = dataSource?.emoticonBoardHeight(keyboardManager: self) ?? 220
        
        subView.frame = CGRect(x: 0, y: CGRectGetHeight(viewController.view.frame), width: CGRectGetWidth(viewController.view.frame), height: height)
        subView.backgroundColor = inputBarView.backgroundColor
        subView.alpha = 0
        viewController.view.addSubview(subView)
        viewController.view.bringSubviewToFront(subView)
        
        return subView
    }()
    
    // 由ViewController实现表情面板view和拓展面板view
    private lazy var extendBoardView: UIView = { [self] in
        
        let subView = dataSource?.extendBoardView(keyboardManager: self) ?? UIView()
        // 默认高度 220
        let height = dataSource?.extendBoardHeight(keyboardManager: self) ?? 220
        
        subView.frame = CGRect(x: 0, y: CGRectGetHeight(viewController.view.frame), width: CGRectGetWidth(viewController.view.frame), height: height)
        subView.alpha = 0
        viewController.view.addSubview(subView)
        viewController.view.bringSubviewToFront(subView)
        
        return subView
    }()
    
    /// iPhoneX 底部距离
    private let safeAreaInsetsBottom: CGFloat = xIphoneBottomSafeHeight
    
    /// iPhoneX系列手机上，如果手指快速向上滑动 tableView，那么会出现 0.1 秒的"可以透过 inputBar 和底部安全区中间看到背景 tableView"。为了解决这个细小的问题，我加入了这个
    private var belowInputBarXView: UIView = UIView()
    
    
    // MARK: - Override Keyboard notifications
    override func onKeyboardWillShowOrHideByNotifications(keyboardRect: CGRect, animationOptions: UIView.AnimationOptions, duration: TimeInterval, showKeyboard: Bool) {
        
        if let textView = inputTextView, !textView.isFirstResponder {
            // 本次弹出软键盘并不是因为我们的输入框，而是有别的输入框
            return
        }
        
        if isViewControllerWillDisappear {
            // 为了解决：键盘弹出状态下vc侧滑返回，会触发Notification进而影响inputBarView.frame的bug
            return
        }
        
        // 马上要弹出键盘了，先看看当前的状态是哪个BoardView，一会要把它隐藏掉
        let state = currentInputState
        
        if showKeyboard {
            // 马上要显示软键盘了，设置当前state为Text模式。
            currentInputState = .text
        }
        
        // 如果不做下面这个 if == InputStateText判断，会导致表情板（或拓展面板）和键盘切换时候inputbar会晃动
        // Q：为什么会抖动？
        // A：软键盘切换到表情面板时：先调用hideKeyboardAndSwitchToCurrentBoardView方法去做动画，这时候State == InputStateEmotion。但是这时候由于软键盘收到了收起通知，也会进入到这里，这里也做了动画。两个动画冲突了
        // 所以加上State == InputStateText，来确保弹出表情板时候不走下面的代码，进而让上述两个动画不会同时执行
        
        // Q：为了避免上述抖动问题，为什么不换成if (showKeyboard)？
        // A：因为如果是单纯的键盘收起其实还是要走下面的代码的；单纯的收起键盘时候showKeyboard为false，State为InputStateText
        
        if currentInputState == .text {
            let keyboardY = viewController.view.convert(keyboardRect, from: nil).origin.y
            
            UIView.animate(withDuration: duration, delay: 0, options: animationOptions) { [self] in
                
                layoutInputBarView(currentBoardViewMinY: keyboardY, boardAllHide: !showKeyboard)
                
                if showKeyboard {
                    // 键盘 Notifition 要 willShow 软键盘
                    switch state {
                    case .emoticon:
                        switchEmotionBoardView(hidden: true)
                    case .extentd:
                        switchExtendBoardView(hidden: true)
                    default: break
                    }
                } else {
                    // 当前本来就已经是软键盘输入文本 && 是键盘Notifition要willHide软键盘
                    // 进入这里的可能性：
                    // 1、软键盘上的“收起键盘”按钮，
                    // 2、点击左边的语音按钮，
                    // 3、vc调用了tv.resignFirstResponder。
                    // 而vc调用hideAll的时候，会被hideKeyboardAndSwitchToCurrentBoardView吸收掉，并不会进入这里
                    currentInputState = .normal
                }
                // 回调给ViewController
                
                callBackCurrentInputViewFrameToScreen(frame: inputBarView.frame, reason: .boardDidHide)
                
                
                //                callBackWholeInputViewHeightDidChange(
                //                    wholeInputViewHeight: viewController.view.frame.size.height
                //                    - inputBarView.frame.origin.y - safeAreaInsetsBottom,
                //                    reason: showKeyboard ? .boardDidShow: .boardDidHide)
            }
        }
    }
}


// MARK: - public
extension InputBoxManager {
    
    /// 添加底部输入框View
    /// - Parameters:
    ///   - inputBarView: inputBarView description
    ///   - belowViewController: YES表示输入框平时不显示（比如朋友圈）；NO表示平时也显示（比如聊天）
    public func addBottomInputBarView(inputBarView: UIView, belowViewController: Bool) {
        // 如果是iphone x，给底部的34pt添加上背景颜色，颜色和输入条一致
        if safeAreaInsetsBottom > 0 && !belowViewController {
            // iPhoneX 且 聊天界面VC（即输入条固定在底部）就会进入这里
            // 我添加了一个和输入条背景颜色一样的普通View在inputView的底部
            belowInputBarXView.frame = CGRect(
                x: 0,
                y: viewController.view.frame.size.height - safeAreaInsetsBottom,
                width: viewController.view.frame.size.width,
                height: safeAreaInsetsBottom
            )
            belowInputBarXView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
            belowInputBarXView.backgroundColor = inputBarView.backgroundColor
            viewController.view.addSubview(belowInputBarXView)
            viewController.view.bringSubviewToFront(belowInputBarXView)
            
        }
        
        // 注意这里，系统会自动调整inputBarView的y和width
        inputBarView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
        
        
        // 工具条的frame
        let inputBarFrame: CGRect = CGRect(
            x: 0,
            y: viewController.view.frame.size.height + (belowViewController ? 0 : (-(inputBarView.frame.size.height + safeAreaInsetsBottom))),
            width: viewController.view.frame.size.width,
            height: inputBarView.frame.size.height
        )
        // vc创建的工具条，本类负责设置frame和addSubview
        inputBarView.frame = inputBarFrame
        
        viewController.view.addSubview(inputBarView)
        viewController.view.bringSubviewToFront(inputBarView)
        
        // 因为底部添加了输入条；回调给vc，让vc处理界面的tableview的frame
        // callBackWholeInputViewHeightDidChange(wholeInputViewHeight: belowViewController ? 0:inputBarView.frame.size.height, reason: .willAddToSuperView)
        callBackCurrentInputViewFrameToScreen(frame: inputBarView.frame, reason: .willAddToSuperView)
        
        self.inputBarView = inputBarView
        isInputBarBelowViewController = belowViewController
        
    }
    
    /// 为了方便动画切换，本 Manager 类需要拿到 textview 或 textField 的引用，
    /// 如果有输入条，请传过来；没有输入条可以不调用该方法
    public func bindTextView(inputTextView: UIResponder) {
        self.inputTextView = inputTextView
    }
    
    /// 隐藏所有面板，包括表情面板和拓展面板 和 软键盘
    public func hideAllBoardView() {
        if currentInputState != .normal {
            hideKeyboardAndSwitchToCurrentBoardView(allBoardHide: true)
        }
    }
    
    /// 表情面板和键盘之间的切换，toEmotionBoardView
    public func switchToEmoticonBoardKeyboard() {
        // 先设置全局变量State为表情State
        currentInputState = .emoticon
        
        // NO表示是要显示当前面板（即InputStateEmotion）
        hideKeyboardAndSwitchToCurrentBoardView(allBoardHide: false)
    }
    
    /// 拓展面板和键盘之间的切换，toExtendBoardView
    public func switchToExtendBoardKeyboard() {
        // 先设置全局变量State为表情State
        currentInputState = .extentd
        
        // NO表示是要显示当前面板（即InputStateEmotion）
        hideKeyboardAndSwitchToCurrentBoardView(allBoardHide: false)
    }
    
    
    /// 底部的输入框高度发生变化，changeValue 高度变化值
    public func inputTextViewHeightDidChange(becauseSendText: Bool) {
        // callBackWholeInputViewHeightDidChange(wholeInputViewHeight: viewController.view.frame.size.height - inputBarView.frame.origin.y - safeAreaInsetsBottom, reason: becauseSendText ? .textDidSend: .textDidChange)
        // callBackCurrentInputViewFrameToScreen(frame: inputBarView.frame, reason: becauseSendText ? .textDidSend: .textDidChange)
    }
    
}

// MARK: - private
extension InputBoxManager {
    /**
     * private 依据当前面板切换，来决定输入条view的frame
     * @param currentBoardViewMinY 当前面板的Frame的minY
     * @param boardAllHide yes 表示所有面板都是隐藏的状态，no 但凡有一个面板没隐藏
     **/
    private func layoutInputBarView(currentBoardViewMinY: CGFloat, boardAllHide: Bool) {
        var prevInputViewFrame = inputBarView!.frame
        
        if boardAllHide {
            // 隐藏掉两个面板
            var hidedFrameY = CGRectGetHeight(viewController.view.bounds)
            if !isInputBarBelowViewController {
                hidedFrameY = hidedFrameY - CGRectGetHeight(prevInputViewFrame)
                hidedFrameY = hidedFrameY - safeAreaInsetsBottom
            }
            prevInputViewFrame.origin.y = hidedFrameY
            
        } else {
            prevInputViewFrame.origin.y = currentBoardViewMinY - CGRectGetHeight(prevInputViewFrame)
        }
        
        inputBarView.frame = prevInputViewFrame
        
        let belowInputBarXViewFrame: CGRect = belowInputBarXView.frame
        belowInputBarXView.frame = CGRectMake(belowInputBarXViewFrame.origin.x, CGRectGetMaxY(inputBarView.frame), belowInputBarXViewFrame.size.width, belowInputBarXViewFrame.size.height)
    }
    
    /// 回调，整个输入框的高度发生改变的时候
    private func callBackCurrentInputViewFrameToScreen(frame: CGRect, reason: InputViewHeightDidChangeReason) {
        delegate?.inputBoxManager?(manager: self, currentInputBarFrame: frame, reson: reason.rawValue)
    }
    
    /// 显示（no）或者 隐藏（yes）表情面板
    /// - Parameter hidden: 显示/隐藏
    /// - Returns: 表情面板的Frame
    @discardableResult
    private func switchEmotionBoardView(hidden: Bool) -> CGRect {
        var prevFrame: CGRect = emotionBoardView.frame
        prevFrame.origin.y = (hidden ? CGRectGetHeight(viewController.view.frame) : (CGRectGetHeight(viewController.view.frame) - CGRectGetHeight(prevFrame)))
        emotionBoardView.alpha = hidden ? 0: 1
        emotionBoardView.frame = prevFrame
        return prevFrame
    }
    
    /// 显示（no）或者 隐藏（yes）拓展面板
    /// - Parameter hidden: 显示/隐藏
    /// - Returns: 拓展面板的Frame
    @discardableResult
    private func switchExtendBoardView(hidden: Bool) -> CGRect {
        var prevFrame: CGRect = extendBoardView.frame
        prevFrame.origin.y = (hidden ? CGRectGetHeight(viewController.view.frame) : (CGRectGetHeight(viewController.view.frame) - CGRectGetHeight(prevFrame)))
        extendBoardView.alpha = hidden ? 0: 1
        extendBoardView.frame = prevFrame
        return prevFrame
    }
    
    
    /**
     * private 切换（或者隐藏）当前面板；  --------->>>>>> 如果是切换，调用该方法前需要设置好currentInputState  <<<<<<<-----------
     * @param allBoardHide yes == 隐藏所有面板，no == 显示currentInputState对应的面板（但是前提是设置好state）
     */
    func hideKeyboardAndSwitchToCurrentBoardView(allBoardHide: Bool) {
        if allBoardHide && currentInputState == .text {
            //隐藏所有面板 && 当前是软键盘，由vc调用->hideAllBoardView(YES)->进入这里
            
            //Q：为什么要在这里就把state设置为Normal？
            //A：因为下面的resignFirstResponder会触发系统Notifications -> 进入onKeyboardWillShowOrHideByNotifications方法
            //  -> if (self.currentInputState == InputStateText) -> self.layoutInputBarView ; self.callBack;
            //
            // 但是本方法下面已经实现了self.layoutInputBarView ; self.callBack;
            // 所以这里把state设置为Normal就是避免再重复走上述方法，上述方法只是处理软键盘上的“收起键盘”按钮
            currentInputState = .normal
        }
        
        // 无论是显示某个面板，还是隐藏所有面板，都要先让输入框失去焦点，然后再进行切换动画
        inputTextView?.resignFirstResponder()
        // 初始化一下面板view。如果在下面的动画代码里才初始化，会导致从左上角飘移进入
        extendBoardView.alpha = 0
        emotionBoardView.alpha = 0
        
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) { [self] in
            var currentBoardViewFrame: CGRect = .zero
            
            if allBoardHide {
                // 表示隐藏掉两个面板
                if currentInputState == .emoticon {
                    currentBoardViewFrame = switchEmotionBoardView(hidden: allBoardHide)
                    
                } else if currentInputState == .extentd {
                    currentBoardViewFrame = switchExtendBoardView(hidden: allBoardHide)
                }
                
            } else {
                // 这里需要注意方法的执行顺序
                if currentInputState == .emoticon {
                    // 1、先隐藏和自己无关的View
                    currentBoardViewFrame = switchExtendBoardView(hidden: !allBoardHide)
                    
                    // 2、再显示和自己相关的View
                    currentBoardViewFrame = switchEmotionBoardView(hidden: allBoardHide)
                    
                } else if currentInputState == .extentd {
                    // 1、先隐藏和自己无关的View
                    currentBoardViewFrame = switchEmotionBoardView(hidden: !allBoardHide)
                    // 2、再显示和自己相关的View
                    currentBoardViewFrame = switchExtendBoardView(hidden: allBoardHide)
                    
                }
            }
            // 重设InputBarView的frame
            layoutInputBarView(currentBoardViewMinY: CGRectGetMinY(currentBoardViewFrame), boardAllHide: allBoardHide)
            // 回调给ViewController
            // callBackWholeInputViewHeightDidChange(wholeInputViewHeight: viewController.view.frame.size.height - inputBarView.frame.origin.y - safeAreaInsetsBottom, reason: allBoardHide ? .boardDidHide: .boardDidShow)
            
            callBackCurrentInputViewFrameToScreen(frame: inputBarView.frame, reason: allBoardHide ? .boardDidHide: .boardDidShow)
            
        } completion: { [self] finished in
            if allBoardHide {
                currentInputState = .normal
            }
        }
        
    }
    
}
